/**
 * Copyright 2018 orivil.com. All rights reserved.
 * Use of this source code is governed by a MIT-style
 * license that can be found at https://mit-license.org.
 */

package access

import (
	"time"
	"encoding/json"
	"net/http"
	"sync"
	"fmt"
	"gitee.com/tugoer/orivil/service"
	"gitee.com/tugoer/orivil/log"
	"gitee.com/tugoer/orivil/timer"
	"gitee.com/tugoer/orivil/wechat"
)

type Config struct {
	AppID string
	Secret string
}

var container = service.NewContainer(true)

type Token struct {
	Value     string        `json:"access_token"`
	ExpiresIn time.Duration `json:"expires_in"`
	ExpiresAt time.Time
	wechat.Error
}

type Ticket struct {
	Value     string        `json:"ticket"`
	ExpiresIn time.Duration `json:"expires_in"`
	ExpiresAt time.Time
	wechat.Error
}

// provider 根据配置的公众号产生, 一个 AppID 对应一个 ticket provider
var providers = struct {
	Access map[string]*service.Provider
	Tickets map[string]*service.Provider
	sync.Mutex
}{
	Access: make(map[string]*service.Provider, 5),
	Tickets: make(map[string]*service.Provider, 5),
}

// 每个微信公众号对应唯一 access provider
func getAccessProvider(w *Config) *service.Provider {
	providers.Lock()
	defer providers.Unlock()
	key := w.AppID + w.Secret
	if pvd, ok := providers.Access[key]; ok {
		return pvd
	} else {
		pvd = newAccessProvider(w)
		providers.Access[key] = pvd
		return pvd
	}
}

// 每个微信公众号对应唯一 ticket provider
func getTicketProvider(w *Config) *service.Provider {
	providers.Lock()
	defer providers.Unlock()
	key := w.AppID + w.Secret
	if pvd, ok := providers.Tickets[key]; ok {
		return pvd
	} else {
		pvd = newTicketProvider(w)
		providers.Tickets[key] = pvd
		return pvd
	}
}

func newAccessProvider(e *Config) *service.Provider {
	var pvd service.Provider = func(c *service.Container) interface{} {
		now := time.Now()
		res, err := http.Get("https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=" + e.AppID + "&secret=" + e.Secret)
		if err != nil {
			log.Emergency.Printf("获取 access_token 出错：%s\n", err)
		} else {
			token := &Token{}
			err = json.NewDecoder(res.Body).Decode(token)
			if err != nil {
				log.Emergency.Printf("解析 access_token 出错：%s\n", err)
			}
			res.Body.Close()
			if token.ErrCode == 0 {
				// 提前 30 分钟结束
				token.ExpiresAt = now.Add((token.ExpiresIn - 1800) * time.Second)
			} else {
				token.ErrMsg = fmt.Sprintf("AppID:%s error: %s", e.AppID, token.ErrMsg)
			}
			return token
		}
		return nil
	}
	return &pvd
}

func newTicketProvider(e *Config) *service.Provider {
	var pvd service.Provider = func(c *service.Container) interface{} {
		now := time.Now()
		if token := GetToken(e); token.ErrCode == 0 {
			ticket := &Ticket{}
			res, err := http.Get("https://api.weixin.qq.com/cgi-bin/ticket/getticket?type=jsapi&access_token=" + token.Value)
			if err != nil {
				ticket.ErrCode = 1
				ticket.ErrMsg = err.Error()
			} else {
				err = json.NewDecoder(res.Body).Decode(ticket)
				if err != nil {
					ticket.ErrCode = 1
					ticket.ErrMsg = fmt.Sprintf("解析 ticket 出错：%s", err)
				}
				res.Body.Close()
				// 获取成功
				if ticket.ErrCode == 0 {
					// 提前 30 分钟结束
					ticket.ExpiresAt = now.Add((ticket.ExpiresIn - 1800) * time.Second)
				} else {
					ticket.ErrMsg = fmt.Sprintf("AppID:%s error: %s", e.AppID, ticket.ErrMsg)
				}
			}
			return ticket
		} else {
			return &Ticket{Error: token.Error}
		}
	}
	return &pvd
}

func GetToken(w *Config) (token *Token) {
	accessProvider := getAccessProvider(w)
	if access, ok := container.Get(accessProvider).(*Token); ok {
		now := timeSection.Section()
		if access.ExpiresAt.After(*now) {
			return access
		}
	}
	if access, ok := container.GetNew(accessProvider).(*Token); ok {
		container.SetCache(accessProvider, access)
		return access
	} else {
		return &Token{Error: wechat.Error{ErrCode: 1, ErrMsg: "获取 access token 出错"}}
	}
}

func GetTicket(w *Config) (ticket *Ticket) {
	ticketProvider := getTicketProvider(w)
	if ticket, ok := container.Get(ticketProvider).(*Ticket); ok {
		now := timeSection.Section()
		if ticket.ExpiresAt.After(*now) {
			return ticket
		}
	}
	ticket = container.GetNew(ticketProvider).(*Ticket)
	container.SetCache(ticketProvider, ticket)
	return ticket
}

var timeSection = timer.NewSectionProvider(5 * time.Minute)